策略权限控制:创建 Policy 服务 & 守卫
策略权限(Policy-Based Access Control)在 RBAC 基础上提供更细粒度的控制。本节从零创建 CaslAbilityService 和 PolicyGuard,利用 CASL 库实现字段级别的权限判断,并理解 ability.can() 和 permittedFieldsOf() 两种权限检查策略。
架构设计:两条查询路径
路径一(接口所需权限): User → Role → Permission → Policy
路径二(用户已有权限): User → Role → Policy
text
- 路径一代表某个路由接口要求哪些策略权限
- 路径二代表用户通过角色拥有哪些已分配的策略权限
PolicyGuard 的核心逻辑就是对比这两条路径的结果。
创建 NestJS 模块结构
生成 Policy 模块
nest g resource policy --no-spec
bash
创建 CaslAbilityService
在 policy 目录下创建 casl-ability/casl-ability.service.ts:
import { Injectable } from '@nestjs/common';
import {
AbilityBuilder,
createMongoAbility,
MongoAbility,
} from '@casl/ability';
@Injectable()
export class CaslAbilityService {
/**
* 为用户创建 ability 实例
* 后续将替换为数据库查询
*/
async register() {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
// TODO: 从数据库读取权限,当前为测试数据
can('read', 'Article');
can('update', 'Article', ['title', 'description'], {
authorId: 11,
});
return build();
}
}
typescript
在 policy.module.ts 中注册并导出服务:
import { Module } from '@nestjs/common';
import { CaslAbilityService } from './casl-ability/casl-ability.service';
@Module({
providers: [CaslAbilityService],
exports: [CaslAbilityService], // 导出以便其他模块注入
})
export class PolicyModule {}
typescript
创建 PolicyGuard
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { CaslAbilityService } from '../../policy/casl-ability/casl-ability.service';
@Injectable()
export class PolicyGuard implements CanActivate {
constructor(private caslAbilityService: CaslAbilityService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const ability = await this.caslAbilityService.register();
// 权限判断逻辑...
return true;
}
}
typescript
在需要使用 Guard 的控制器所在模块中导入 PolicyModule:
// user.module.ts
import { PolicyModule } from '../policy/policy.module';
@Module({
imports: [PolicyModule],
// ...
})
export class UserModule {}
typescript
CASL 权限判断的核心概念
can 方法的参数含义
can(action, subject, fields?, conditions?)
typescript
| 参数 | 说明 | 示例 |
|---|---|---|
| action | 操作类型 | 'read', 'update', 'manage' |
| subject | 资源类型/实例 | 'Article' 或 articleInstance |
| fields | 允许操作的字段 | ['title', 'description'] |
| conditions | 条件对象 | { authorId: 11 } |
两种权限检查策略
策略一:ability.can() 布尔判断
import { AbilityBuilder, createMongoAbility } from '@casl/ability';
function defineAbilityFor(user) {
const { can, build } = new AbilityBuilder(createMongoAbility);
can('read', 'Article');
can('update', 'Article', ['title', 'description'], { authorId: user.id });
return build();
}
const ability = defineAbilityFor({ id: 11 });
const article = new Article();
article.authorId = 11;
ability.can('update', article, 'title'); // true
ability.can('update', article, 'published'); // false - 不在允许字段内
typescript
策略二:permittedFieldsOf() 获取允许字段列表
import { permittedFieldsOf } from '@casl/ability/extra';
const fields = permittedFieldsOf(ability, 'update', article);
// => ['title', 'description'] (条件匹配时)
// => [] (条件不匹配时)
typescript
Subject 类型对判断结果的影响
CASL 对 subject 的判断逻辑有严格区分:
// 情况 1:传入字符串(只检查类型,不检查条件)
ability.can('update', 'Article');
// => true(如果定义了该 action+subject)
// 情况 2:传入类实例(同时检查 conditions)
ability.can('update', articleInstance);
// => 取决于 articleInstance 是否满足 conditions
typescript
| 测试场景 | can 定义 | subject 参数 | 结果 |
|---|---|---|---|
| 字符串 subject | can('update', 'Article', ['title']) | 'Article' | true |
| 实例匹配条件 | can('update', 'Article', ['title'], { authorId: 11 }) | article (authorId=11) | true |
| 实例不匹配 | 同上 | article (authorId=12) | false |
PolicyGuard 的设计思路
PolicyGuard 执行流程
│
├─ 1. CaslAbilityService.register()
│ 从数据库读取用户角色的 Policy,构建 ability 实例
│
├─ 2. 读取当前路由的 metadata
│ 获取接口要求的 Policy(action, subject, fields, conditions)
│
├─ 3. 策略一:ability.can(action, subjectInstance, field)
│ 直接返回布尔值:有权限 / 无权限
│
└─ 4. 策略二:permittedFieldsOf(ability, action, subject)
返回用户被允许操作的字段列表(细粒度到字段)
text
两种策略分别适用于不同场景:接口级别的通过/拒绝使用 can(),需要精确控制字段访问权限时使用 permittedFieldsOf()。
↑